package domain;

import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;

/**
 * Thread that watches a directory for changes <br />
 * If the thread detects a change it will call the client to do a certain thing
 *
 * @author ParaSait
 */
public class DirectoryListener extends Thread {

    private Client client;
    private boolean running;
    private boolean activated;
    private WatchService watcher;
    private Map<WatchKey, Path> keys = new ConcurrentHashMap<WatchKey, Path>();

    /**
     * Constructor
     *
     * @param client The client that should be notified on changes
     */
    public DirectoryListener(Client client) {
        this.client = client;
    }

    /**
     * Set the thread to running. <br />
     * Setting this to false will stop the thread. <br />
     * Thread can't be continued without restarting it.
     *
     * @param running
     */
    public void setRunning(boolean running) {
        this.running = running;
    }

    /**
     * Set the thread to activated. <br />
     * Setting to false will lock the thread. <br />
     * Setting to true will unlock the thread (thread will continue to run).
     *
     * @param activated
     */
    public void setActivated(boolean activated) {
        this.activated = activated;
    }
    
    /**
     * Registers a directory with the listener. <br />
     * From this moment the directory will be watched for changes.
     *
     * @param dir The location of the directory (as a string) that needs to be watched.
     * @throws IOException If there is an error reading the directory.
     */
    public void registerDirectory(String dir) throws IOException {
        Path path = Paths.get(dir);
        registerDirectory(path);
    }

    /**
     * Registers a directory with the listener. <br />
     * From this moment the directory will be watched for changes.
     *
     * @param dir The location of the directory (as a path) that needs to be watched.
     * @throws IOException If there is an arror reading the directory.
     */
    public void registerDirectory(Path dir) throws IOException {
        try {
            if(watcher == null)
                watcher = FileSystems.getDefault().newWatchService();
            WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
            keys.put(key, dir);
        } catch (IOException ioe) {
            throw new IOException("Error reading directory.");
        }
    }
    
    /**
     * Registers a directory and its subdirectories with the listener. <br />
     * From this moment the directory will be watched for changes.
     * 
     * @param startDirectory The location of the directory (as a string) that needs to be watched
     * @throws IOException If there is an error reading the directory.
     */
    public void registerDirectoryAndSubDirectories(String startDirectory) throws IOException {
        Path path = Paths.get(startDirectory);
        registerDirectoryAndSubDirectories(path);
    }
    
    /**
     * Registers a directory and its subdirectories with the listener. <br />
     * From this moment the directory will be watched for changes.
     * 
     * @param startDirectory The location of the directory (as a path) that needs to be watched
     * @throws IOException If there is an error reading the directory.
     */
    public void registerDirectoryAndSubDirectories(final Path startDirectory) throws IOException {
        Files.walkFileTree(startDirectory, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                throws IOException
            {
                registerDirectory(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
    
    private void setLastModifiedDates(File file) {
        File dir = new File(file.getAbsolutePath());
        for (File child : dir.listFiles()) {
            if (child.isFile()) {
                Date date = new Date();
                child.setLastModified(date.getTime());
            }
            if (child.isDirectory()) {
                setLastModifiedDates(child);
            }
        }
    }

    @Override
    public void run() {
        WatchKey key;
        setRunning(true);
        while (running) {
            if (activated && !client.isPerformingInitialSync()) {
                key = null;
                try {
                    key = watcher.poll(10, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    setRunning(false);
                    break;
                } catch (ClosedWatchServiceException e) {
                    e.printStackTrace();
                    setRunning(false);
                    break;
                }

                if (key != null) {
                    Path path = keys.get(key);
                    for (WatchEvent<?> i : key.pollEvents()) {
                        WatchEvent<Path> event = (WatchEvent<Path>) i;
                        WatchEvent.Kind<Path> kind = event.kind();
                        Path name = event.context();
                        
                        String rootDirectory = client.getRootDirectory();
                        File file = new File(path + "\\" + name.toString());
                        
                        String absolutePath = file.getAbsolutePath();
                        String relativePath = absolutePath.replace(rootDirectory + "\\", "");
                        
                        if (!file.isDirectory()) {
                            if (file.exists() && kind == ENTRY_MODIFY) {
                                client.deleteFileOnServer(relativePath);
                                client.uploadFileToServer(relativePath);
                            } else if(file.exists() && kind == ENTRY_CREATE) {
                                client.uploadFileToServer(relativePath);
                            }else if (kind == ENTRY_DELETE) {
                                client.deleteFileOnServer(relativePath);
                            }
                        }
                        else {
                            try {
                                registerDirectoryAndSubDirectories(file.getAbsolutePath());
                                setLastModifiedDates(file);
                            } catch (IOException ioe) {
                                ioe.printStackTrace();
                            }
                        }
                    }
                    if (key.reset() == false) {
                        keys.remove(key);
                        if (keys.isEmpty()) {
                            break;
                        }
                    }
                }
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
            }
        }

        try {
            watcher.close();
        } catch (IOException ex) {
        }
    }
}
